// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using FluentAssertions;
using Microsoft.DotNet.Cli.Build.Framework;
using Microsoft.DotNet.CoreSetup.Test;
using Microsoft.NET.HostModel.AppHost;
using Xunit;
using static Microsoft.DotNet.CoreSetup.Test.NetCoreAppBuilder;
using static Microsoft.NET.HostModel.AppHost.HostWriter;

namespace HostActivation.Tests
{
    public class SelfContainedAppLaunch : IClassFixture<SelfContainedAppLaunch.SharedTestState>
    {
        private SharedTestState sharedTestState;

        public SelfContainedAppLaunch(SelfContainedAppLaunch.SharedTestState fixture)
        {
            sharedTestState = fixture;
        }

        [Fact]
        public void Default()
        {
            string appExe = sharedTestState.App.AppExe;
            if (Binaries.CetCompat.IsSupported)
                Assert.True(Binaries.CetCompat.IsMarkedCompatible(appExe));

            Command.Create(appExe)
                .CaptureStdErr()
                .CaptureStdOut()
                .Execute()
                .Should().Pass()
                .And.HaveStdOutContaining("Hello World")
                .And.HaveStdOutContaining(HostTestContext.MicrosoftNETCoreAppVersion);

            if (OperatingSystem.IsWindows())
            {
                // App sets FileVersion to NETCoreApp version. On Windows, this should be copied to app resources.
                string expectedVersion = HostTestContext.MicrosoftNETCoreAppVersion.Contains('-')
                    ? HostTestContext.MicrosoftNETCoreAppVersion[..HostTestContext.MicrosoftNETCoreAppVersion.IndexOf('-')]
                    : HostTestContext.MicrosoftNETCoreAppVersion;
                Assert.Equal($"{expectedVersion}.0", System.Diagnostics.FileVersionInfo.GetVersionInfo(appExe).FileVersion);
            }
        }

        [Fact]
        public void AppHost_DisableCetCompat()
        {
            Assert.SkipUnless(Binaries.CetCompat.IsSupported, "CET not supported on this platform");

            TestApp app = sharedTestState.App.Copy();
            app.CreateAppHost(disableCetCompat: true);
            Assert.False(Binaries.CetCompat.IsMarkedCompatible(app.AppExe));

            Command.Create(app.AppExe)
                .CaptureStdErr()
                .CaptureStdOut()
                .Execute()
                .Should().Pass()
                .And.HaveStdOutContaining("Hello World")
                .And.HaveStdOutContaining(HostTestContext.MicrosoftNETCoreAppVersion);
        }

        [Fact]
        public void NoDepsJson_NoRuntimeConfig()
        {
            var app = sharedTestState.App.Copy();

            File.Delete(app.RuntimeConfigJson);
            File.Delete(app.DepsJson);

            // Make sure normal run succeeds and doesn't print any errors
            Command.Create(app.AppExe)
                .CaptureStdErr()
                .CaptureStdOut()
                .Execute()
                .Should().Pass()
                // Note that this is an exact match - we don't expect any output from the host itself
                .And.HaveStdOut($"Hello World!{Environment.NewLine}{Environment.NewLine}.NET {HostTestContext.MicrosoftNETCoreAppVersion}{Environment.NewLine}")
                .And.NotHaveStdErr();

            // Make sure tracing indicates there is no runtime config and no deps json
            Command.Create(app.AppExe)
                .EnableTracingAndCaptureOutputs()
                .Execute()
                .Should().Pass()
                .And.HaveStdOut($"Hello World!{Environment.NewLine}{Environment.NewLine}.NET {HostTestContext.MicrosoftNETCoreAppVersion}{Environment.NewLine}")
                .And.HaveStdErrContaining($"Runtime config does not exist at [{app.RuntimeConfigJson}]")
                .And.HaveStdErrContaining($"Dependencies manifest does not exist at [{app.DepsJson}]");
        }

        [Fact]
        public void RenameApphost()
        {
            var app = sharedTestState.App.Copy();

            var renamedAppExe = app.AppExe + Binaries.GetExeName("renamed");
            File.Move(app.AppExe, renamedAppExe, true);

            Command.Create(renamedAppExe)
                .CaptureStdErr()
                .CaptureStdOut()
                .Execute()
                .Should().Pass()
                .And.HaveStdOutContaining("Hello World")
                .And.HaveStdOutContaining(HostTestContext.MicrosoftNETCoreAppVersion);
        }

        [Fact]
        public void RelativeEmbeddedPath()
        {
            var app = sharedTestState.App.Copy();

            // Delete the apphost
            File.Delete(app.AppExe);

            // Create an apphost in a subdirectory pointing at the app using relative path
            string subDir = Path.Combine(app.Location, "sub");
            Directory.CreateDirectory(subDir);
            string appExe = Path.Combine(subDir, Path.GetFileName(app.AppExe));
            HostWriter.CreateAppHost(
                Binaries.AppHost.FilePath,
                appExe,
                Path.GetRelativePath(subDir, app.AppDll),
                enableMacOSCodeSign: RuntimeInformation.IsOSPlatform(OSPlatform.OSX));

            Command.Create(appExe)
                .CaptureStdErr()
                .CaptureStdOut()
                .Execute()
                .Should().Pass()
                .And.HaveStdOutContaining("Hello World")
                .And.HaveStdOutContaining(HostTestContext.MicrosoftNETCoreAppVersion);
        }

        [Fact]
        public void DotNetRoot_IncorrectLayout_Fails()
        {
            var app = sharedTestState.App.Copy();

            // Move the apphost exe and app dll to a subdirectory
            string subDir = Path.Combine(app.Location, "sub");
            Directory.CreateDirectory(subDir);
            string appExe = Path.Combine(subDir, Path.GetFileName(app.AppExe));
            File.Move(app.AppExe, appExe);
            File.Move(app.AppDll, Path.Combine(subDir, Path.GetFileName(app.AppDll)));

            // This verifies a self-contained apphost cannot use DOTNET_ROOT to reference a flat
            // self-contained layout since a flat layout of the shared framework is not supported.
            Command.Create(appExe)
                .EnableTracingAndCaptureOutputs()
                .DotNetRoot(app.Location)
                .Execute()
                .Should().Fail()
                .And.HaveUsedDotNetRootInstallLocation(Path.GetFullPath(app.Location), HostTestContext.BuildRID)
                .And.HaveStdErrContaining($"The required library {Binaries.HostFxr.FileName} could not be found.");
        }

                [Fact]
        [PlatformSpecific(TestPlatforms.Windows)]
        public void Exe_activation_of_GUI_App()
        {
            string appExe = $@"\\?\{sharedTestState.App.AppExe}";
            Command.Create(appExe)
                .CaptureStdErr()
                .CaptureStdOut()
                .Execute()
                .Should().Pass()
                .And.HaveStdOutContaining("Hello World")
                .And.HaveStdOutContaining(HostTestContext.MicrosoftNETCoreAppVersion);

            appExe = $@"\\.\{sharedTestState.App.AppExe}";
            Command.Create(appExe)
                .CaptureStdErr()
                .CaptureStdOut()
                .Execute()
                .Should().Pass()
                .And.HaveStdOutContaining("Hello World")
                .And.HaveStdOutContaining(HostTestContext.MicrosoftNETCoreAppVersion);
        }

        [Fact]
        public void CustomRuntimeLocation()
        {
            string subdirectory = "runtime-directory";
            Action<NetCoreAppBuilder> customizer = b =>
            {
                // Find the NETCoreApp runtime pack
                RuntimeLibraryBuilder netCoreApp = b.RuntimeLibraries.First(
                    r => r.Type == RuntimeLibraryType.runtimepack.ToString() && r.Name == $"runtimepack.{Constants.MicrosoftNETCoreApp}.Runtime.{HostTestContext.BuildRID}");

                // Update all NETCoreApp asset paths to point to the subdirectory
                RuntimeAssetGroupBuilder[] groups = [.. netCoreApp.AssemblyGroups, .. netCoreApp.NativeLibraryGroups];
                foreach (RuntimeAssetGroupBuilder group in groups)
                {
                    foreach (RuntimeFileBuilder asset in group.Assets)
                    {
                        asset.Path = Path.Join(subdirectory, asset.Path);
                    }
                }

                foreach (ResourceAssemblyBuilder resource in netCoreApp.ResourceAssemblies)
                {
                    resource.Path = Path.Join(subdirectory, resource.Path);
                }
            };

            using TestApp app = TestApp.CreateFromBuiltAssets("HelloWorld");
            app.PopulateSelfContained(TestApp.MockedComponent.None, customizer);
            app.CreateAppHost(dotNetRootOptions: new()
            {
                Location = DotNetSearchOptions.SearchLocation.AppRelative,
                AppRelativeDotNet = subdirectory
            });

            // Apphost should be able to find hostfxr based on the .NET search options
            // Runtime files should be resolved based on relative path in .deps.json
            Command.Create(app.AppExe)
                .EnableTracingAndCaptureOutputs()
                .Execute()
                .Should().Pass()
                .And.HaveStdOutContaining("Hello World")
                .And.HaveStdOutContaining(HostTestContext.MicrosoftNETCoreAppVersion)
                .And.HaveStdErrContaining($"CoreCLR path = '{Path.Join(app.Location, subdirectory, Binaries.CoreClr.FileName)}'");
        }

        public class SharedTestState : IDisposable
        {
            public TestApp App { get; }

            public SharedTestState()
            {
                App = TestApp.CreateFromBuiltAssets("HelloWorld");
                App.PopulateSelfContained(TestApp.MockedComponent.None);
                App.CreateAppHost();
            }

            public void Dispose()
            {
                App.Dispose();
            }
        }
    }
}
